Three Layer Haskell Cake
Haskellにおけるデータのレイヤリングおよびそれによる機能の分離方法について。
Problem
free monad, mtl style, monad transformerなどアプローチが色々あって選べない。
適材適所で使い分けることを前提に、それぞれを適切に織り込もう。
Solution
ReaderT Pattern
code:haskell
newtype AppT m a
= AppT
{ unAppT :: ReaderT YourStuff m a
} deriving (Functor, Applicative, Monad, etc)
上位レイヤがどう動くかをハンドルする。
リトライや非同期処理など、アプリケーションの中でも低レベルな部分の制御を表現する。
Pros
上位レイヤのコードをクリーンに保てそう。
Cons
低レベルな制御を扱うためテストが難しい。このレイヤにビジネスロジックを集めるとテストで苦労する。
なので何かロジックをこのレイヤーに埋める時は必ず、Input/Outputを定めて純粋な関数として保つようにする。
mtl Style
code:haskell
class MonadTime m where
getCurrentTime :: m UTCTime
外部サービスへの依存部分を抽象化するために挿入されたレイヤ。
この例では現在の時間をimpureに取得するインスタンスとして
code:haskell
instance MonadTime IO where
getCurrentTime :: IO UTCTime
getCurrentTime = ...
のようにわかりやすいが純粋でない実装を渡すことができる一方で、pureに取得するインスタンスとして
code:haskell
instance MonadTime ((->) UTCTime) where
getCurrentTime = id
こう書くこともできる。
更に現実的な例として
code:haskell
class Monad m => MonadLock m where
acquireLock
:: NominalDiffTime
-> Key
-> m (Maybe Lock)
renewLock
:: NominalDiffTime
-> Lock
-> m (Maybe Lock)
releaseLock
:: Lock
-> m ()
これは分散ロック機構をイメージした場合だ。商用であればRedisなどとのコミュニケーションが実装になるだろう。
ユニットテスト用の環境では IORef (Map ByteString ByteString) のような型が代用できるだろう。
他にもDBとコミュニケーションするために
code:haskell
class (Monad m) => AcquireUser m where
getUserBy :: UserQuery -> m User getUser :: UserId -> m (Maybe User)
getUserWithDig :: UserId -> m (Maybe (User, Dog))
class AcquireUser m => UpdateUser m where
deleteUser :: UserId -> m ()
insertUser :: User -> m ()
ユーザー User というドメインに対するアクセス手段を提供するフロントエンドだ。
バックエンドはDBかもしれないし他のWeb APIかもしれない。それはインスタンス次第で選べる。
This layer excels at providing swappable implementations of external services.
これに尽きる。
Business Logic
単純なデータと純粋関数からなる。
All the effectful data should have been acquired beforehand, and all effectful post-processing should be handled afterwards.
ちょうどRe-frameの概念と照応するな、と思った。
Effectful data beforehand: Coeffect
Effectful post-processing afterward: Effect
Src -> Pipe -> Sink の両端にEffectが起きるとすると、ビジネスロジックはこの1セットが複数回動作することでユースケースを実現している。
例
DBから結果セット fetch -> データの変換 -> KVSに書き込み ->
Web APIを呼び出して認証・認可 -> レスポンスにあるトークンを用いて別microserviceのRPC呼び出し
これらの場合 -> の両端は副作用だし、これらを組み合わせる可能性だってある。
つまりこのEffectfulな一連のステップが一つの合成単位 = 射としてビジネスロジックを構成すると言える。
Further Reading
所感
改めて見直すとレイヤリングをそれぞれ別の要素技術で実現するのがとても不思議に見えるのだった。
Snoymanは下記のような呼称を記していた。
L1: Imperative programming
L2: Object oriented programming
L3: Functional programming
若干こじつけっぽいのでもう少しいい捉え方がありそう。
L1: Infrastructure
L2: Adapter
L3: Domain
じゃないかな。
その上で実用を考えると、